iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Rust

大家一起跟Rust當好朋友吧!系列 第 20

Day 20: 選擇你的 Web 框架:Actix Web vs. Axum

  • 分享至 

  • xImage
  •  

嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第二十天!

昨天我們完成了個人部落格後端的完整規劃,從個人創作者的角度分析了需求,設計了簡潔而實用的資料模型和 API 架構。今天我們要面臨一個重要的決定:為我們的個人部落格選擇最合適的 Web 框架

在 Rust 的 Web 開發生態系中,有兩個框架特別受到關注:Actix WebAxum。今天我們就從個人專案的角度來深入比較這兩個框架,並最終為我們的個人部落格做出選擇!

個人專案的選擇考量

對於個人部落格專案,我們的關注點和大型企業專案會有所不同:

個人開發者的需求

  • 學習成本:我們通常是一個人開發,需要框架容易上手
  • 維護簡單:部署後希望能穩定運行,不需要頻繁維護
  • 資源有限:可能運行在小型 VPS 上,對效能有要求
  • 擴展性:未來可能會增加功能,框架要支援
  • 文件品質:遇到問題時能快速找到解答

讓我們從這些角度來比較兩個框架!

Actix Web:久經考驗的選擇

Actix Web 是 Rust 生態系中最早成熟的 Web 框架之一,在個人專案中有什麼優勢?

🚀 對個人開發者的優勢

1. 成熟穩定,問題少

use actix_web::{web, App, HttpResponse, HttpServer, Result};

// 簡單直接的 API 設計
async fn get_post(path: web::Path<i32>) -> Result<HttpResponse> {
    let post_id = path.into_inner();
    // 個人部落格通常文章不多,簡單查詢即可
    let post = fetch_post_from_db(post_id).await;
    Ok(HttpResponse::Ok().json(post))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/posts/{id}", web::get().to(get_post))
    })
    .bind("127.0.0.1:3000")?
    .run()
    .await
}

2. 豐富的生態和範例
對於個人開發者來說,能找到現成的解決方案非常重要:

  • 大量的部落格文章和教學資源
  • Stack Overflow 上有豐富的 Q&A
  • 很多開源專案可以參考

3. 效能優秀
個人 VPS 資源有限,Actix Web 的高效能表現是一大優勢。

🤔 對個人開發者的挑戰

1. 學習曲線相對陡峭

// 錯誤處理需要較多樣板程式碼
async fn create_post(post_data: web::Json<CreatePostRequest>) -> Result<HttpResponse> {
    match validate_post_data(&post_data) {
        Ok(_) => {
            match save_post_to_db(post_data.into_inner()).await {
                Ok(post) => Ok(HttpResponse::Ok().json(post)),
                Err(e) => {
                    log::error!("儲存文章失敗: {}", e);
                    Ok(HttpResponse::InternalServerError().json("儲存失敗"))
                }
            }
        }
        Err(validation_errors) => {
            Ok(HttpResponse::BadRequest().json(validation_errors))
        }
    }
}

2. 較多的手動處理
對於個人專案,我們希望能專注於業務邏輯,而不是框架的細節。

Axum:現代化的個人專案首選

Axum 是相對較新的框架,它對個人開發者有什麼吸引力?

✨ 對個人開發者的優勢

1. 開發體驗極佳

use axum::{
    extract::{Path, Query},
    response::Json,
    routing::{get, post},
    Router,
};
use serde::{Deserialize, Serialize};

// 型別安全,編譯時就能發現錯誤
async fn get_post(Path(post_id): Path<i32>) -> Result<Json<Post>, AppError> {
    let post = fetch_post_from_db(post_id).await?; // ? 運算子自動處理錯誤
    Ok(Json(post))
}

// 查詢參數自動解析
#[derive(Deserialize)]
struct PostQuery {
    tag: Option<String>,
    page: Option<u32>,
}

async fn list_posts(Query(params): Query<PostQuery>) -> Json<Vec<Post>> {
    let posts = fetch_posts_with_filter(params.tag, params.page.unwrap_or(1)).await;
    Json(posts)
}

// 簡潔的路由定義
fn create_app() -> Router {
    Router::new()
        .route("/posts", get(list_posts).post(create_post))
        .route("/posts/:id", get(get_post))
}

2. 錯誤處理超級友善

use axum::{response::IntoResponse, http::StatusCode};
use thiserror::Error;

// 定義一次,處處可用
#[derive(Error, Debug)]
enum AppError {
    #[error("資料庫錯誤")]
    Database(#[from] sqlx::Error),
    #[error("文章 #{0} 不存在")]
    PostNotFound(i32),
    #[error("無效的請求: {0}")]
    BadRequest(String),
}

// 自動轉換為 HTTP 回應
impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, message) = match self {
            AppError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "系統暫時無法使用"),
            AppError::PostNotFound(id) => (StatusCode::NOT_FOUND, format!("找不到文章 #{}", id)),
            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
        };
        
        (status, message).into_response()
    }
}

// 處理器變得超級簡潔
async fn get_post(Path(post_id): Path<i32>) -> Result<Json<Post>, AppError> {
    let post = fetch_post_from_db(post_id)
        .await?
        .ok_or(AppError::PostNotFound(post_id))?;
    
    Ok(Json(post))
}

3. 模組化設計,完美適合個人專案成長

// 輕鬆組織代碼,隨著功能增加而擴展
mod posts {
    use super::*;
    
    pub fn routes() -> Router {
        Router::new()
            .route("/", get(list_posts).post(create_post))
            .route("/:id", get(get_post).put(update_post).delete(delete_post))
            .route("/:id/comments", get(get_comments).post(add_comment))
    }
    
    // ... 處理器實作
}

mod admin {
    use super::*;
    
    pub fn routes() -> Router {
        Router::new()
            .route("/posts", get(admin_list_posts))
            .route("/comments/pending", get(pending_comments))
            // 需要認證的管理功能
            .layer(auth_middleware())
    }
}

// 主應用程式
pub fn create_app() -> Router {
    Router::new()
        .nest("/api/posts", posts::routes())
        .nest("/api/admin", admin::routes())
        .route("/health", get(health_check))
}

4. 與 Tokio 生態完美整合
對個人專案來說,這意味著:

  • 使用同一套非同步生態系統
  • 性能優化和資源使用更有效率
  • 未來學習其他 Tokio 相關技術更容易

🤔 Axum 的小小挑戰

1. 相對較新

  • 社群相對較小(但成長很快)
  • 第三方教學資源較少

2. 需要學習 Tower 概念

  • 但其實對個人項目來說,基本用法就夠了

個人部落格的具體比較

讓我們看看兩個框架實現個人部落格常見功能的程式碼對比:

文章列表 API

Actix Web 版本

async fn get_posts(
    query: web::Query<PostQuery>,
) -> Result<HttpResponse, actix_web::Error> {
    let page = query.page.unwrap_or(1);
    let per_page = query.per_page.unwrap_or(10).min(50);
    let tag = query.tag.as_deref();
    
    match fetch_posts(page, per_page, tag).await {
        Ok(posts) => {
            let response = PostListResponse {
                posts,
                page,
                per_page,
                total: count_posts(tag).await.unwrap_or(0),
            };
            Ok(HttpResponse::Ok().json(response))
        }
        Err(e) => {
            log::error!("查詢文章失敗: {}", e);
            Ok(HttpResponse::InternalServerError().json("查詢失敗"))
        }
    }
}

Axum 版本

async fn get_posts(
    Query(params): Query<PostQuery>,
) -> Result<Json<PostListResponse>, AppError> {
    let page = params.page.unwrap_or(1);
    let per_page = params.per_page.unwrap_or(10).min(50);
    
    let posts = fetch_posts(page, per_page, params.tag.as_deref()).await?;
    let total = count_posts(params.tag.as_deref()).await?;
    
    Ok(Json(PostListResponse {
        posts,
        page,
        per_page,
        total,
    }))
}

明顯可以看出 Axum 版本更簡潔,錯誤處理更優雅!

為個人部落格選擇 Axum 的原因

經過深入比較,我決定為我們的個人部落格選擇 Axum

1. 🎯 學習投資報酬率高

學 Axum = 學 Tokio 生態系統 = 未來 Rust 非同步開發的基石

2. 🛠️ 開發效率高

個人時間有限,Axum 讓我們能專注於功能實現,而不是框架細節

3. 🔒 型別安全減少 Bug

一人開發最怕的就是線上出問題,Axum 在編譯時就能發現很多錯誤

4. 📈 成長空間大

從簡單部落格到複雜應用,Axum 的架構都能支撐

5. 🌟 現代化設計理念

代表 Rust Web 開發的未來方向

開始建立個人部落格專案

讓我們用 Axum 建立專案骨架:

1. 專案初始化

# 建立個人部落格專案
cargo new blog
cd blog

2. 核心依賴

Cargo.toml 中添加:

[dependencies]
axum = { version = "0.8.4", features = ["macros"] }
tower = "0.5.2"
tower-http = { version = "0.6.6", features = ["cors", "trace"] }

# 非同步運行時
tokio = { version = "1.0", features = ["full"] }

# JSON 處理 - 個人部落格必備
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenvy = "0.15"
# OpenAPI / Swagger UI
utoipa = { version = "5", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }

# 日期時間 - 文章發布時間
chrono = { version = "0.4", features = ["serde"] }

# 簡單的日誌
tracing = "0.1"
tracing-subscriber = "0.3"

# 錯誤處理 - 讓個人維護更輕鬆
thiserror = "2.0.16"

3. 第一個簡潔的個人部落格 API

// src/main.rs
use axum::{routing::get, Json, Router};
use serde::Serialize;
use tokio::net::TcpListener;
use std::env;

// ---- 新增:utoipa 匯入 ----
use utoipa::{OpenApi, ToSchema};
use utoipa_swagger_ui::SwaggerUi;

#[derive(Serialize, ToSchema)]
struct BlogInfo {
    name: String,
    description: String,
    author: String,
    status: String,
}

#[utoipa::path(
    get,
    path = "/",
    tag = "blog",
    responses(
        (status = 200, description = "取得部落格資訊", body = BlogInfo)
    )
)]
async fn blog_info() -> Json<BlogInfo> {
    Json(BlogInfo {
        name: "我的個人技術部落格".to_string(),
        description: "分享程式設計學習心得與生活感悟".to_string(),
        author: "你的名字".to_string(),
        status: "running".to_string(),
    })
}

#[utoipa::path(
    get,
    path = "/health",
    tag = "health",
    responses(
        (status = 200, description = "健康檢查 OK", body = String)
    )
)]
async fn health_check() -> &'static str {
    "個人部落格運行正常!"
}

// ---- 新增:彙整 API 文件 ----
#[derive(OpenApi)]
#[openapi(
    paths(
        blog_info,
        health_check,
    ),
    components(
        schemas(BlogInfo)
    ),
    tags(
        (name = "blog", description = "部落格資訊相關 API"),
        (name = "health", description = "服務健康檢查")
    )
)]
struct ApiDoc;

#[tokio::main]
async fn main() {
    // 載入 .env(沒有就略過)
    let _ = dotenvy::dotenv();

    // 從環境變數讀 HOST/PORT,沒有就用預設
    let protocol = env::var("PROTOCOL").unwrap_or_else(|_| "http".into());
    let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".into());
    let port = env::var("PORT").unwrap_or_else(|_| "3000".into());
    let addr = format!("{host}:{port}");

    // 你的原本路由
    let app = Router::new()
        .route("/", get(blog_info))
        .route("/health", get(health_check))
        .merge(
            SwaggerUi::new("/docs")
                .url("/api-docs/openapi.json", ApiDoc::openapi()),
        );

    let listener = TcpListener::bind(&addr).await.unwrap();
    println!("🚀 個人部落格服務啟動於 {protocol}://{addr} ;Swagger UI 在 {protocol}://{addr}/docs");
    axum::serve(listener, app).await.unwrap();
}

注意:我們故意保持簡潔,沒有加入日誌、錯誤處理等複雜功能。這些會在接下來的日子裡逐步加入!

4. 測試個人部落格

cargo run

測試 API:

# 查看部落格資訊
curl http://127.0.0.1:3000/
# {"name":"我的個人技術部落格","description":"分享程式設計學習心得與生活感悟",...}

# 健康檢查
curl http://127.0.0.1:3000/health
# 個人部落格運行正常!

個人專案的框架選擇指南

選擇 Axum 如果你:

  • ✅ 重視開發效率和代碼簡潔性
  • ✅ 希望有良好的型別安全保障
  • ✅ 想學習現代 Rust 技術棧
  • ✅ 計劃長期維護和擴展專案
  • ✅ 希望錯誤處理簡單優雅

選擇 Actix Web 如果你:

  • ✅ 需要極致的效能(但通常個人部落格用不到)
  • ✅ 習慣傳統的 Web 框架風格
  • ✅ 需要大量的第三方擴展
  • ✅ 團隊已經熟悉 Actix Web

個人建議
對於個人部落格專案,Axum 是更好的選擇。它讓你能專注於創作內容管理的邏輯,而不是框架的複雜性。

今天的收穫

今天我們從個人開發者的角度深入比較了兩大 Web 框架:

個人專案視角

  • ✅ 理解個人專案和企業專案的不同需求
  • ✅ 分析學習成本、維護負擔、擴展性等因素
  • ✅ 從實際使用體驗比較兩個框架

技術決策

  • ✅ 選擇 Axum 作為個人部落格的開發框架
  • ✅ 理解型別安全和開發效率的重要性
  • ✅ 設定基本的專案結構

實戰準備

  • ✅ 建立第一個個人部落格 API
  • ✅ 實現基本的資訊和健康檢查端點
  • ✅ 為明天的深入開發做好準備

明天預告

明天我們將進入 Day 21:個人部落格實戰準備:工具箱與開發環境

準備好用 Rust 打造專屬於你的部落格平台了嗎?我們明天見!


上一篇
Day 19: 開始規劃我的個人部落格後端
系列文
大家一起跟Rust當好朋友吧!20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言